package com.trilead.ssh2.channel;

import java.io.IOException;
import java.util.HashMap;
import java.util.Vector;

import com.trilead.ssh2.AuthAgentCallback;
import com.trilead.ssh2.ChannelCondition;
import com.trilead.ssh2.log.Logger;
import com.trilead.ssh2.packets.PacketChannelAuthAgentReq;
import com.trilead.ssh2.packets.PacketChannelOpenConfirmation;
import com.trilead.ssh2.packets.PacketChannelOpenFailure;
import com.trilead.ssh2.packets.PacketChannelTrileadPing;
import com.trilead.ssh2.packets.PacketGlobalCancelForwardRequest;
import com.trilead.ssh2.packets.PacketGlobalForwardRequest;
import com.trilead.ssh2.packets.PacketGlobalTrileadPing;
import com.trilead.ssh2.packets.PacketOpenDirectTCPIPChannel;
import com.trilead.ssh2.packets.PacketOpenSessionChannel;
import com.trilead.ssh2.packets.PacketSessionExecCommand;
import com.trilead.ssh2.packets.PacketSessionPtyRequest;
import com.trilead.ssh2.packets.PacketSessionPtyResize;
import com.trilead.ssh2.packets.PacketSessionStartShell;
import com.trilead.ssh2.packets.PacketSessionSubsystemRequest;
import com.trilead.ssh2.packets.PacketSessionX11Request;
import com.trilead.ssh2.packets.Packets;
import com.trilead.ssh2.packets.TypesReader;
import com.trilead.ssh2.transport.MessageHandler;
import com.trilead.ssh2.transport.TransportManager;

/**
 * ChannelManager. Please read the comments in Channel.java.
 * <p>
 * Besides the crypto part, this is the core of the library.
 * 
 * @author Christian Plattner, plattner@trilead.com
 * @version $Id: ChannelManager.java,v 1.2 2008/03/03 07:01:36 cplattne Exp $
 */
public class ChannelManager implements MessageHandler {
	private static final Logger log = Logger.getLogger(ChannelManager.class);

	private HashMap x11_magic_cookies = new HashMap();

	private TransportManager tm;

	private Vector channels = new Vector();
	private int nextLocalChannel = 100;
	private boolean shutdown = false;
	private int globalSuccessCounter = 0;
	private int globalFailedCounter = 0;

	private HashMap remoteForwardings = new HashMap();

	private AuthAgentCallback authAgent;

	private Vector listenerThreads = new Vector();

	private boolean listenerThreadsAllowed = true;

	public ChannelManager(TransportManager tm) {
		this.tm = tm;
		tm.registerMessageHandler(this, 80, 100);
	}

	private int addChannel(Channel c) {
		synchronized (channels) {
			channels.addElement(c);
			return nextLocalChannel++;
		}
	}

	public X11ServerData checkX11Cookie(String hexFakeCookie) {
		synchronized (x11_magic_cookies) {
			if (hexFakeCookie != null)
				return (X11ServerData) x11_magic_cookies.get(hexFakeCookie);
		}
		return null;
	}

	public void closeAllChannels() {
		if (log.isEnabled())
			log.log(50, "Closing all channels");

		Vector channel_copy;

		synchronized (channels) {
			channel_copy = (Vector) channels.clone();
		}

		for (int i = 0; i < channel_copy.size(); i++) {
			Channel c = (Channel) channel_copy.elementAt(i);
			try {
				closeChannel(c, "Closing all channels", true);
			} catch (IOException e) {
			}
		}
	}

	public void closeChannel(Channel c, String reason, boolean force)
			throws IOException {
		byte msg[] = new byte[5];

		synchronized (c) {
			if (force) {
				c.state = Channel.STATE_CLOSED;
				c.EOF = true;
			}

			c.setReasonClosed(reason);

			msg[0] = Packets.SSH_MSG_CHANNEL_CLOSE;
			msg[1] = (byte) (c.remoteID >> 24);
			msg[2] = (byte) (c.remoteID >> 16);
			msg[3] = (byte) (c.remoteID >> 8);
			msg[4] = (byte) (c.remoteID);

			c.notifyAll();
		}

		synchronized (c.channelSendLock) {
			if (c.closeMessageSent == true)
				return;
			tm.sendMessage(msg);
			c.closeMessageSent = true;
		}

		if (log.isEnabled())
			log.log(50, "Sent SSH_MSG_CHANNEL_CLOSE (channel " + c.localID
					+ ")");
	}

	public int getAvailable(Channel c, boolean extended) throws IOException {
		synchronized (c) {
			int avail;

			if (extended)
				avail = c.stderrWritepos - c.stderrReadpos;
			else
				avail = c.stdoutWritepos - c.stdoutReadpos;

			return ((avail > 0) ? avail : (c.EOF ? -1 : 0));
		}
	}

	private Channel getChannel(int id) {
		synchronized (channels) {
			for (int i = 0; i < channels.size(); i++) {
				Channel c = (Channel) channels.elementAt(i);
				if (c.localID == id)
					return c;
			}
		}
		return null;
	}

	public int getChannelData(Channel c, boolean extended, byte[] target,
			int off, int len) throws IOException {
		int copylen = 0;
		int increment = 0;
		int remoteID = 0;
		int localID = 0;

		synchronized (c) {
			int stdoutAvail = 0;
			int stderrAvail = 0;

			while (true) {
				/*
				 * Data available? We have to return remaining data even if the
				 * channel is already closed.
				 */

				stdoutAvail = c.stdoutWritepos - c.stdoutReadpos;
				stderrAvail = c.stderrWritepos - c.stderrReadpos;

				if ((!extended) && (stdoutAvail != 0))
					break;

				if ((extended) && (stderrAvail != 0))
					break;

				/* Do not wait if more data will never arrive (EOF or CLOSED) */

				if ((c.EOF) || (c.state != Channel.STATE_OPEN))
					return -1;

				try {
					c.wait();
				} catch (InterruptedException ignore) {
				}
			}

			/* OK, there is some data. Return it. */

			if (!extended) {
				copylen = (stdoutAvail > len) ? len : stdoutAvail;
				System.arraycopy(c.stdoutBuffer, c.stdoutReadpos, target, off,
						copylen);
				c.stdoutReadpos += copylen;

				if (c.stdoutReadpos != c.stdoutWritepos)

					System.arraycopy(c.stdoutBuffer, c.stdoutReadpos,
							c.stdoutBuffer, 0, c.stdoutWritepos
									- c.stdoutReadpos);

				c.stdoutWritepos -= c.stdoutReadpos;
				c.stdoutReadpos = 0;
			} else {
				copylen = (stderrAvail > len) ? len : stderrAvail;
				System.arraycopy(c.stderrBuffer, c.stderrReadpos, target, off,
						copylen);
				c.stderrReadpos += copylen;

				if (c.stderrReadpos != c.stderrWritepos)

					System.arraycopy(c.stderrBuffer, c.stderrReadpos,
							c.stderrBuffer, 0, c.stderrWritepos
									- c.stderrReadpos);

				c.stderrWritepos -= c.stderrReadpos;
				c.stderrReadpos = 0;
			}

			if (c.state != Channel.STATE_OPEN)
				return copylen;

			if (c.localWindow < ((Channel.CHANNEL_BUFFER_SIZE + 1) / 2)) {
				int minFreeSpace = Math.min(Channel.CHANNEL_BUFFER_SIZE
						- c.stdoutWritepos, Channel.CHANNEL_BUFFER_SIZE
						- c.stderrWritepos);

				increment = minFreeSpace - c.localWindow;
				c.localWindow = minFreeSpace;
			}

			remoteID = c.remoteID; /* read while holding the lock */
			localID = c.localID; /* read while holding the lock */
		}

		/*
		 * If a consumer reads stdout and stdin in parallel, we may end up with
		 * sending two msgWindowAdjust messages. Luckily, it does not matter in
		 * which order they arrive at the server.
		 */

		if (increment > 0) {
			if (log.isEnabled())
				log.log(80, "Sending SSH_MSG_CHANNEL_WINDOW_ADJUST (channel "
						+ localID + ", " + increment + ")");

			synchronized (c.channelSendLock) {
				byte[] msg = c.msgWindowAdjust;

				msg[0] = Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST;
				msg[1] = (byte) (remoteID >> 24);
				msg[2] = (byte) (remoteID >> 16);
				msg[3] = (byte) (remoteID >> 8);
				msg[4] = (byte) (remoteID);
				msg[5] = (byte) (increment >> 24);
				msg[6] = (byte) (increment >> 16);
				msg[7] = (byte) (increment >> 8);
				msg[8] = (byte) (increment);

				if (c.closeMessageSent == false)
					tm.sendMessage(msg);
			}
		}

		return copylen;
	}

	@Override
	public void handleMessage(byte[] msg, int msglen) throws IOException {
		if (msg == null) {
			if (log.isEnabled())
				log.log(50, "HandleMessage: got shutdown");

			synchronized (listenerThreads) {
				for (int i = 0; i < listenerThreads.size(); i++) {
					IChannelWorkerThread lat = (IChannelWorkerThread) listenerThreads
							.elementAt(i);
					lat.stopWorking();
				}
				listenerThreadsAllowed = false;
			}

			synchronized (channels) {
				shutdown = true;

				for (int i = 0; i < channels.size(); i++) {
					Channel c = (Channel) channels.elementAt(i);
					synchronized (c) {
						c.EOF = true;
						c.state = Channel.STATE_CLOSED;
						c.setReasonClosed("The connection is being shutdown");
						c.closeMessageRecv = true; /*
													 * You never know, perhaps
													 * we are waiting for a
													 * pending close message
													 * from the server...
													 */
						c.notifyAll();
					}
				}
				/* Works with J2ME */
				channels.setSize(0);
				channels.trimToSize();
				channels.notifyAll(); /* Notify global response waiters */
				return;
			}
		}

		switch (msg[0]) {
		case Packets.SSH_MSG_CHANNEL_OPEN_CONFIRMATION:
			msgChannelOpenConfirmation(msg, msglen);
			break;
		case Packets.SSH_MSG_CHANNEL_WINDOW_ADJUST:
			msgChannelWindowAdjust(msg, msglen);
			break;
		case Packets.SSH_MSG_CHANNEL_DATA:
			msgChannelData(msg, msglen);
			break;
		case Packets.SSH_MSG_CHANNEL_EXTENDED_DATA:
			msgChannelExtendedData(msg, msglen);
			break;
		case Packets.SSH_MSG_CHANNEL_REQUEST:
			msgChannelRequest(msg, msglen);
			break;
		case Packets.SSH_MSG_CHANNEL_EOF:
			msgChannelEOF(msg, msglen);
			break;
		case Packets.SSH_MSG_CHANNEL_OPEN:
			msgChannelOpen(msg, msglen);
			break;
		case Packets.SSH_MSG_CHANNEL_CLOSE:
			msgChannelClose(msg, msglen);
			break;
		case Packets.SSH_MSG_CHANNEL_SUCCESS:
			msgChannelSuccess(msg, msglen);
			break;
		case Packets.SSH_MSG_CHANNEL_FAILURE:
			msgChannelFailure(msg, msglen);
			break;
		case Packets.SSH_MSG_CHANNEL_OPEN_FAILURE:
			msgChannelOpenFailure(msg, msglen);
			break;
		case Packets.SSH_MSG_GLOBAL_REQUEST:
			msgGlobalRequest(msg, msglen);
			break;
		case Packets.SSH_MSG_REQUEST_SUCCESS:
			msgGlobalSuccess();
			break;
		case Packets.SSH_MSG_REQUEST_FAILURE:
			msgGlobalFailure();
			break;
		default:
			throw new IOException("Cannot handle unknown channel message "
					+ (msg[0] & 0xff));
		}
	}

	public void msgChannelClose(byte[] msg, int msglen) throws IOException {
		if (msglen != 5)
			throw new IOException(
					"SSH_MSG_CHANNEL_CLOSE message has wrong size (" + msglen
							+ ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16)
				| ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException(
					"Unexpected SSH_MSG_CHANNEL_CLOSE message for non-existent channel "
							+ id);

		synchronized (c) {
			c.EOF = true;
			c.state = Channel.STATE_CLOSED;
			c.setReasonClosed("Close requested by remote");
			c.closeMessageRecv = true;

			removeChannel(c.localID);

			c.notifyAll();
		}

		if (log.isEnabled())
			log.log(50, "Got SSH_MSG_CHANNEL_CLOSE (channel " + id + ")");
	}

	public void msgChannelData(byte[] msg, int msglen) throws IOException {
		if (msglen <= 9)
			throw new IOException(
					"SSH_MSG_CHANNEL_DATA message has wrong size (" + msglen
							+ ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16)
				| ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);
		int len = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16)
				| ((msg[7] & 0xff) << 8) | (msg[8] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException(
					"Unexpected SSH_MSG_CHANNEL_DATA message for non-existent channel "
							+ id);

		if (len != (msglen - 9))
			throw new IOException(
					"SSH_MSG_CHANNEL_DATA message has wrong len (calculated "
							+ (msglen - 9) + ", got " + len + ")");

		if (log.isEnabled())
			log.log(80, "Got SSH_MSG_CHANNEL_DATA (channel " + id + ", " + len
					+ ")");

		synchronized (c) {
			if (c.state == Channel.STATE_CLOSED)
				return; // ignore

			if (c.state != Channel.STATE_OPEN)
				throw new IOException(
						"Got SSH_MSG_CHANNEL_DATA, but channel is not in correct state ("
								+ c.state + ")");

			if (c.localWindow < len)
				throw new IOException(
						"Remote sent too much data, does not fit into window.");

			c.localWindow -= len;

			System.arraycopy(msg, 9, c.stdoutBuffer, c.stdoutWritepos, len);
			c.stdoutWritepos += len;

			c.notifyAll();
		}
	}

	public void msgChannelEOF(byte[] msg, int msglen) throws IOException {
		if (msglen != 5)
			throw new IOException(
					"SSH_MSG_CHANNEL_EOF message has wrong size (" + msglen
							+ ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16)
				| ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException(
					"Unexpected SSH_MSG_CHANNEL_EOF message for non-existent channel "
							+ id);

		synchronized (c) {
			c.EOF = true;
			c.notifyAll();
		}

		if (log.isEnabled())
			log.log(50, "Got SSH_MSG_CHANNEL_EOF (channel " + id + ")");
	}

	public void msgChannelExtendedData(byte[] msg, int msglen)
			throws IOException {
		if (msglen <= 13)
			throw new IOException(
					"SSH_MSG_CHANNEL_EXTENDED_DATA message has wrong size ("
							+ msglen + ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16)
				| ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);
		int dataType = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16)
				| ((msg[7] & 0xff) << 8) | (msg[8] & 0xff);
		int len = ((msg[9] & 0xff) << 24) | ((msg[10] & 0xff) << 16)
				| ((msg[11] & 0xff) << 8) | (msg[12] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException(
					"Unexpected SSH_MSG_CHANNEL_EXTENDED_DATA message for non-existent channel "
							+ id);

		if (dataType != Packets.SSH_EXTENDED_DATA_STDERR)
			throw new IOException(
					"SSH_MSG_CHANNEL_EXTENDED_DATA message has unknown type ("
							+ dataType + ")");

		if (len != (msglen - 13))
			throw new IOException(
					"SSH_MSG_CHANNEL_EXTENDED_DATA message has wrong len (calculated "
							+ (msglen - 13) + ", got " + len + ")");

		if (log.isEnabled())
			log.log(80, "Got SSH_MSG_CHANNEL_EXTENDED_DATA (channel " + id
					+ ", " + len + ")");

		synchronized (c) {
			if (c.state == Channel.STATE_CLOSED)
				return; // ignore

			if (c.state != Channel.STATE_OPEN)
				throw new IOException(
						"Got SSH_MSG_CHANNEL_EXTENDED_DATA, but channel is not in correct state ("
								+ c.state + ")");

			if (c.localWindow < len)
				throw new IOException(
						"Remote sent too much data, does not fit into window.");

			c.localWindow -= len;

			System.arraycopy(msg, 13, c.stderrBuffer, c.stderrWritepos, len);
			c.stderrWritepos += len;

			c.notifyAll();
		}
	}

	public void msgChannelFailure(byte[] msg, int msglen) throws IOException {
		if (msglen != 5)
			throw new IOException(
					"SSH_MSG_CHANNEL_FAILURE message has wrong size (" + msglen
							+ ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16)
				| ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException(
					"Unexpected SSH_MSG_CHANNEL_FAILURE message for non-existent channel "
							+ id);

		synchronized (c) {
			c.failedCounter++;
			c.notifyAll();
		}

		if (log.isEnabled())
			log.log(50, "Got SSH_MSG_CHANNEL_FAILURE (channel " + id + ")");
	}

	public void msgChannelOpen(byte[] msg, int msglen) throws IOException {
		TypesReader tr = new TypesReader(msg, 0, msglen);

		tr.readByte(); // skip packet type
		String channelType = tr.readString();
		int remoteID = tr.readUINT32(); /* sender channel */
		int remoteWindow = tr.readUINT32(); /* initial window size */
		int remoteMaxPacketSize = tr.readUINT32(); /* maximum packet size */

		if ("x11".equals(channelType)) {
			synchronized (x11_magic_cookies) {
				/*
				 * If we did not request X11 forwarding, then simply ignore this
				 * bogus request.
				 */

				if (x11_magic_cookies.size() == 0) {
					PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(
							remoteID,
							Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED,
							"X11 forwarding not activated", "");

					tm.sendAsynchronousMessage(pcof.getPayload());

					if (log.isEnabled())
						log.log(20, "Unexpected X11 request, denying it!");

					return;
				}
			}

			String remoteOriginatorAddress = tr.readString();
			int remoteOriginatorPort = tr.readUINT32();

			Channel c = new Channel(this);

			synchronized (c) {
				c.remoteID = remoteID;
				c.remoteWindow = remoteWindow & 0xFFFFffffL; /*
															 * properly convert
															 * UINT32 to long
															 */
				c.remoteMaxPacketSize = remoteMaxPacketSize;
				c.localID = addChannel(c);
			}

			/*
			 * The open confirmation message will be sent from another thread
			 */

			RemoteX11AcceptThread rxat = new RemoteX11AcceptThread(c,
					remoteOriginatorAddress, remoteOriginatorPort);
			rxat.setDaemon(true);
			rxat.start();

			return;
		}

		if ("forwarded-tcpip".equals(channelType)) {
			String remoteConnectedAddress = tr.readString(); /*
															 * address that was
															 * connected
															 */
			int remoteConnectedPort = tr.readUINT32(); /*
														 * port that was
														 * connected
														 */
			String remoteOriginatorAddress = tr.readString(); /*
															 * originator IP
															 * address
															 */
			int remoteOriginatorPort = tr.readUINT32(); /* originator port */

			RemoteForwardingData rfd = null;

			synchronized (remoteForwardings) {
				rfd = (RemoteForwardingData) remoteForwardings.get(new Integer(
						remoteConnectedPort));
			}

			if (rfd == null) {
				PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(
						remoteID, Packets.SSH_OPEN_ADMINISTRATIVELY_PROHIBITED,
						"No thanks, unknown port in forwarded-tcpip request",
						"");

				/* Always try to be polite. */

				tm.sendAsynchronousMessage(pcof.getPayload());

				if (log.isEnabled())
					log.log(20,
							"Unexpected forwarded-tcpip request, denying it!");

				return;
			}

			Channel c = new Channel(this);

			synchronized (c) {
				c.remoteID = remoteID;
				c.remoteWindow = remoteWindow & 0xFFFFffffL; /*
															 * convert UINT32 to
															 * long
															 */
				c.remoteMaxPacketSize = remoteMaxPacketSize;
				c.localID = addChannel(c);
			}

			/*
			 * The open confirmation message will be sent from another thread.
			 */

			RemoteAcceptThread rat = new RemoteAcceptThread(c,
					remoteConnectedAddress, remoteConnectedPort,
					remoteOriginatorAddress, remoteOriginatorPort,
					rfd.targetAddress, rfd.targetPort);

			rat.setDaemon(true);
			rat.start();

			return;
		}

		if ("auth-agent@openssh.com".equals(channelType)) {
			Channel c = new Channel(this);

			synchronized (c) {
				c.remoteID = remoteID;
				c.remoteWindow = remoteWindow & 0xFFFFffffL; /*
															 * properly convert
															 * UINT32 to long
															 */
				c.remoteMaxPacketSize = remoteMaxPacketSize;
				c.localID = addChannel(c);
			}

			AuthAgentForwardThread aat = new AuthAgentForwardThread(c,
					authAgent);

			aat.setDaemon(true);
			aat.start();

			return;
		}

		/* Tell the server that we have no idea what it is talking about */

		PacketChannelOpenFailure pcof = new PacketChannelOpenFailure(remoteID,
				Packets.SSH_OPEN_UNKNOWN_CHANNEL_TYPE, "Unknown channel type",
				"");

		tm.sendAsynchronousMessage(pcof.getPayload());

		if (log.isEnabled())
			log.log(20, "The peer tried to open an unsupported channel type ("
					+ channelType + ")");
	}

	public void msgChannelOpenConfirmation(byte[] msg, int msglen)
			throws IOException {
		PacketChannelOpenConfirmation sm = new PacketChannelOpenConfirmation(
				msg, 0, msglen);

		Channel c = getChannel(sm.recipientChannelID);

		if (c == null)
			throw new IOException(
					"Unexpected SSH_MSG_CHANNEL_OPEN_CONFIRMATION message for non-existent channel "
							+ sm.recipientChannelID);

		synchronized (c) {
			if (c.state != Channel.STATE_OPENING)
				throw new IOException(
						"Unexpected SSH_MSG_CHANNEL_OPEN_CONFIRMATION message for channel "
								+ sm.recipientChannelID);

			c.remoteID = sm.senderChannelID;
			c.remoteWindow = sm.initialWindowSize & 0xFFFFffffL; /*
																 * convert
																 * UINT32 to
																 * long
																 */
			c.remoteMaxPacketSize = sm.maxPacketSize;
			c.state = Channel.STATE_OPEN;
			c.notifyAll();
		}

		if (log.isEnabled())
			log.log(50, "Got SSH_MSG_CHANNEL_OPEN_CONFIRMATION (channel "
					+ sm.recipientChannelID + " / remote: "
					+ sm.senderChannelID + ")");
	}

	public void msgChannelOpenFailure(byte[] msg, int msglen)
			throws IOException {
		if (msglen < 5)
			throw new IOException(
					"SSH_MSG_CHANNEL_OPEN_FAILURE message has wrong size ("
							+ msglen + ")");

		TypesReader tr = new TypesReader(msg, 0, msglen);

		tr.readByte(); // skip packet type
		int id = tr.readUINT32(); /* sender channel */

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException(
					"Unexpected SSH_MSG_CHANNEL_OPEN_FAILURE message for non-existent channel "
							+ id);

		int reasonCode = tr.readUINT32();
		String description = tr.readString("UTF-8");

		String reasonCodeSymbolicName = null;

		switch (reasonCode) {
		case 1:
			reasonCodeSymbolicName = "SSH_OPEN_ADMINISTRATIVELY_PROHIBITED";
			break;
		case 2:
			reasonCodeSymbolicName = "SSH_OPEN_CONNECT_FAILED";
			break;
		case 3:
			reasonCodeSymbolicName = "SSH_OPEN_UNKNOWN_CHANNEL_TYPE";
			break;
		case 4:
			reasonCodeSymbolicName = "SSH_OPEN_RESOURCE_SHORTAGE";
			break;
		default:
			reasonCodeSymbolicName = "UNKNOWN REASON CODE (" + reasonCode + ")";
		}

		StringBuffer descriptionBuffer = new StringBuffer();
		descriptionBuffer.append(description);

		for (int i = 0; i < descriptionBuffer.length(); i++) {
			char cc = descriptionBuffer.charAt(i);

			if ((cc >= 32) && (cc <= 126))
				continue;
			descriptionBuffer.setCharAt(i, '\uFFFD');
		}

		synchronized (c) {
			c.EOF = true;
			c.state = Channel.STATE_CLOSED;
			c.setReasonClosed("The server refused to open the channel ("
					+ reasonCodeSymbolicName + ", '"
					+ descriptionBuffer.toString() + "')");
			c.notifyAll();
		}

		if (log.isEnabled())
			log.log(50, "Got SSH_MSG_CHANNEL_OPEN_FAILURE (channel " + id + ")");
	}

	public void msgChannelRequest(byte[] msg, int msglen) throws IOException {
		TypesReader tr = new TypesReader(msg, 0, msglen);

		tr.readByte(); // skip packet type
		int id = tr.readUINT32();

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException(
					"Unexpected SSH_MSG_CHANNEL_REQUEST message for non-existent channel "
							+ id);

		String type = tr.readString("US-ASCII");
		boolean wantReply = tr.readBoolean();

		if (log.isEnabled())
			log.log(80, "Got SSH_MSG_CHANNEL_REQUEST (channel " + id + ", '"
					+ type + "')");

		if (type.equals("exit-status")) {
			if (wantReply != false)
				throw new IOException(
						"Badly formatted SSH_MSG_CHANNEL_REQUEST message, 'want reply' is true");

			int exit_status = tr.readUINT32();

			if (tr.remain() != 0)
				throw new IOException(
						"Badly formatted SSH_MSG_CHANNEL_REQUEST message");

			synchronized (c) {
				c.exit_status = new Integer(exit_status);
				c.notifyAll();
			}

			if (log.isEnabled())
				log.log(50, "Got EXIT STATUS (channel " + id + ", status "
						+ exit_status + ")");

			return;
		}

		if (type.equals("exit-signal")) {
			if (wantReply != false)
				throw new IOException(
						"Badly formatted SSH_MSG_CHANNEL_REQUEST message, 'want reply' is true");

			String signame = tr.readString("US-ASCII");
			tr.readBoolean();
			tr.readString();
			tr.readString();

			if (tr.remain() != 0)
				throw new IOException(
						"Badly formatted SSH_MSG_CHANNEL_REQUEST message");

			synchronized (c) {
				c.exit_signal = signame;
				c.notifyAll();
			}

			if (log.isEnabled())
				log.log(50, "Got EXIT SIGNAL (channel " + id + ", signal "
						+ signame + ")");

			return;
		}

		/*
		 * We simply ignore unknown channel requests, however, if the server
		 * wants a reply, then we signal that we have no idea what it is about.
		 */

		if (wantReply) {
			byte[] reply = new byte[5];

			reply[0] = Packets.SSH_MSG_CHANNEL_FAILURE;
			reply[1] = (byte) (c.remoteID >> 24);
			reply[2] = (byte) (c.remoteID >> 16);
			reply[3] = (byte) (c.remoteID >> 8);
			reply[4] = (byte) (c.remoteID);

			tm.sendAsynchronousMessage(reply);
		}

		if (log.isEnabled())
			log.log(50, "Channel request '" + type
					+ "' is not known, ignoring it");
	}

	public void msgChannelSuccess(byte[] msg, int msglen) throws IOException {
		if (msglen != 5)
			throw new IOException(
					"SSH_MSG_CHANNEL_SUCCESS message has wrong size (" + msglen
							+ ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16)
				| ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException(
					"Unexpected SSH_MSG_CHANNEL_SUCCESS message for non-existent channel "
							+ id);

		synchronized (c) {
			c.successCounter++;
			c.notifyAll();
		}

		if (log.isEnabled())
			log.log(80, "Got SSH_MSG_CHANNEL_SUCCESS (channel " + id + ")");
	}

	public void msgChannelWindowAdjust(byte[] msg, int msglen)
			throws IOException {
		if (msglen != 9)
			throw new IOException(
					"SSH_MSG_CHANNEL_WINDOW_ADJUST message has wrong size ("
							+ msglen + ")");

		int id = ((msg[1] & 0xff) << 24) | ((msg[2] & 0xff) << 16)
				| ((msg[3] & 0xff) << 8) | (msg[4] & 0xff);
		int windowChange = ((msg[5] & 0xff) << 24) | ((msg[6] & 0xff) << 16)
				| ((msg[7] & 0xff) << 8) | (msg[8] & 0xff);

		Channel c = getChannel(id);

		if (c == null)
			throw new IOException(
					"Unexpected SSH_MSG_CHANNEL_WINDOW_ADJUST message for non-existent channel "
							+ id);

		synchronized (c) {
			final long huge = 0xFFFFffffL; /* 2^32 - 1 */

			c.remoteWindow += (windowChange & huge); /* avoid sign extension */

			/* TODO - is this a good heuristic? */

			if ((c.remoteWindow > huge))
				c.remoteWindow = huge;

			c.notifyAll();
		}

		if (log.isEnabled())
			log.log(80, "Got SSH_MSG_CHANNEL_WINDOW_ADJUST (channel " + id
					+ ", " + windowChange + ")");
	}

	public void msgGlobalFailure() throws IOException {
		synchronized (channels) {
			globalFailedCounter++;
			channels.notifyAll();
		}

		if (log.isEnabled())
			log.log(80, "Got SSH_MSG_REQUEST_FAILURE");
	}

	public void msgGlobalRequest(byte[] msg, int msglen) throws IOException {
		/* Currently we do not support any kind of global request */

		TypesReader tr = new TypesReader(msg, 0, msglen);

		tr.readByte(); // skip packet type
		String requestName = tr.readString();
		boolean wantReply = tr.readBoolean();

		if (wantReply) {
			byte[] reply_failure = new byte[1];
			reply_failure[0] = Packets.SSH_MSG_REQUEST_FAILURE;

			tm.sendAsynchronousMessage(reply_failure);
		}

		/* We do not clean up the requestName String - that is OK for debug */

		if (log.isEnabled())
			log.log(80, "Got SSH_MSG_GLOBAL_REQUEST (" + requestName + ")");
	}

	public void msgGlobalSuccess() throws IOException {
		synchronized (channels) {
			globalSuccessCounter++;
			channels.notifyAll();
		}

		if (log.isEnabled())
			log.log(80, "Got SSH_MSG_REQUEST_SUCCESS");
	}

	public Channel openDirectTCPIPChannel(String host_to_connect,
			int port_to_connect, String originator_IP_address,
			int originator_port) throws IOException {
		Channel c = new Channel(this);

		synchronized (c) {
			c.localID = addChannel(c);
			// end of synchronized block forces writing out to main memory
		}

		PacketOpenDirectTCPIPChannel dtc = new PacketOpenDirectTCPIPChannel(
				c.localID, c.localWindow, c.localMaxPacketSize,
				host_to_connect, port_to_connect, originator_IP_address,
				originator_port);

		tm.sendMessage(dtc.getPayload());

		waitUntilChannelOpen(c);

		return c;
	}

	public Channel openSessionChannel() throws IOException {
		Channel c = new Channel(this);

		synchronized (c) {
			c.localID = addChannel(c);
			// end of synchronized block forces the writing out to main memory
		}

		if (log.isEnabled())
			log.log(50, "Sending SSH_MSG_CHANNEL_OPEN (Channel " + c.localID
					+ ")");

		PacketOpenSessionChannel smo = new PacketOpenSessionChannel(c.localID,
				c.localWindow, c.localMaxPacketSize);
		tm.sendMessage(smo.getPayload());

		waitUntilChannelOpen(c);

		return c;
	}

	public void registerThread(IChannelWorkerThread thr) throws IOException {
		synchronized (listenerThreads) {
			if (listenerThreadsAllowed == false)
				throw new IOException("Too late, this connection is closed.");
			listenerThreads.addElement(thr);
		}
	}

	public void registerX11Cookie(String hexFakeCookie, X11ServerData data) {
		synchronized (x11_magic_cookies) {
			x11_magic_cookies.put(hexFakeCookie, data);
		}
	}

	private void removeChannel(int id) {
		synchronized (channels) {
			for (int i = 0; i < channels.size(); i++) {
				Channel c = (Channel) channels.elementAt(i);
				if (c.localID == id) {
					channels.removeElementAt(i);
					break;
				}
			}
		}
	}

	public void requestCancelGlobalForward(int bindPort) throws IOException {
		RemoteForwardingData rfd = null;

		synchronized (remoteForwardings) {
			rfd = (RemoteForwardingData) remoteForwardings.get(new Integer(
					bindPort));

			if (rfd == null)
				throw new IOException(
						"Sorry, there is no known remote forwarding for remote port "
								+ bindPort);
		}

		synchronized (channels) {
			globalSuccessCounter = globalFailedCounter = 0;
		}

		PacketGlobalCancelForwardRequest pgcf = new PacketGlobalCancelForwardRequest(
				true, rfd.bindAddress, rfd.bindPort);
		tm.sendMessage(pgcf.getPayload());

		if (log.isEnabled())
			log.log(50, "Requesting cancelation of remote forward ('"
					+ rfd.bindAddress + "', " + rfd.bindPort + ")");

		try {
			if (waitForGlobalRequestResult() == false)
				throw new IOException("The server denied the request.");
		} finally {
			synchronized (remoteForwardings) {
				/*
				 * Only now we are sure that no more forwarded connections will
				 * arrive
				 */
				remoteForwardings.remove(rfd);
			}
		}

	}

	/**
	 * @param agent
	 * @throws IOException
	 */
	public boolean requestChannelAgentForwarding(Channel c,
			AuthAgentCallback authAgent) throws IOException {
		synchronized (this) {
			if (this.authAgent != null)
				throw new IllegalStateException("Auth agent already exists");

			this.authAgent = authAgent;
		}

		synchronized (channels) {
			globalSuccessCounter = globalFailedCounter = 0;
		}

		if (log.isEnabled())
			log.log(50, "Requesting agent forwarding");

		PacketChannelAuthAgentReq aar = new PacketChannelAuthAgentReq(
				c.remoteID);
		tm.sendMessage(aar.getPayload());

		if (waitForChannelRequestResult(c) == false) {
			authAgent = null;
			return false;
		}

		return true;
	}

	public void requestChannelTrileadPing(Channel c) throws IOException {
		PacketChannelTrileadPing pctp;

		synchronized (c) {
			if (c.state != Channel.STATE_OPEN)
				throw new IOException("Cannot ping this channel ("
						+ c.getReasonClosed() + ")");

			pctp = new PacketChannelTrileadPing(c.remoteID);

			c.successCounter = c.failedCounter = 0;
		}

		synchronized (c.channelSendLock) {
			if (c.closeMessageSent)
				throw new IOException("Cannot ping this channel ("
						+ c.getReasonClosed() + ")");
			tm.sendMessage(pctp.getPayload());
		}

		try {
			if (waitForChannelRequestResult(c) == true)
				throw new IOException(
						"Your server is alive - but buggy. "
								+ "It replied with SSH_MSG_SESSION_SUCCESS when it actually should not.");

		} catch (IOException e) {
			throw (IOException) new IOException("The ping request failed.")
					.initCause(e);
		}
	}

	public void requestExecCommand(Channel c, String cmd) throws IOException {
		PacketSessionExecCommand sm;

		synchronized (c) {
			if (c.state != Channel.STATE_OPEN)
				throw new IOException(
						"Cannot execute command on this channel ("
								+ c.getReasonClosed() + ")");

			sm = new PacketSessionExecCommand(c.remoteID, true, cmd);

			c.successCounter = c.failedCounter = 0;
		}

		synchronized (c.channelSendLock) {
			if (c.closeMessageSent)
				throw new IOException(
						"Cannot execute command on this channel ("
								+ c.getReasonClosed() + ")");
			tm.sendMessage(sm.getPayload());
		}

		if (log.isEnabled())
			log.log(50, "Executing command (channel " + c.localID + ", '" + cmd
					+ "')");

		try {
			if (waitForChannelRequestResult(c) == false)
				throw new IOException("The server denied the request.");
		} catch (IOException e) {
			throw (IOException) new IOException("The execute request failed.")
					.initCause(e);
		}
	}

	public int requestGlobalForward(String bindAddress, int bindPort,
			String targetAddress, int targetPort) throws IOException {
		RemoteForwardingData rfd = new RemoteForwardingData();

		rfd.bindAddress = bindAddress;
		rfd.bindPort = bindPort;
		rfd.targetAddress = targetAddress;
		rfd.targetPort = targetPort;

		synchronized (remoteForwardings) {
			Integer key = new Integer(bindPort);

			if (remoteForwardings.get(key) != null) {
				throw new IOException(
						"There is already a forwarding for remote port "
								+ bindPort);
			}

			remoteForwardings.put(key, rfd);
		}

		synchronized (channels) {
			globalSuccessCounter = globalFailedCounter = 0;
		}

		PacketGlobalForwardRequest pgf = new PacketGlobalForwardRequest(true,
				bindAddress, bindPort);
		tm.sendMessage(pgf.getPayload());

		if (log.isEnabled())
			log.log(50, "Requesting a remote forwarding ('" + bindAddress
					+ "', " + bindPort + ")");

		try {
			if (waitForGlobalRequestResult() == false)
				throw new IOException(
						"The server denied the request (did you enable port forwarding?)");
		} catch (IOException e) {
			synchronized (remoteForwardings) {
				remoteForwardings.remove(rfd);
			}
			throw e;
		}

		return bindPort;
	}

	public void requestGlobalTrileadPing() throws IOException {
		synchronized (channels) {
			globalSuccessCounter = globalFailedCounter = 0;
		}

		PacketGlobalTrileadPing pgtp = new PacketGlobalTrileadPing();

		tm.sendMessage(pgtp.getPayload());

		if (log.isEnabled())
			log.log(50, "Sending SSH_MSG_GLOBAL_REQUEST 'trilead-ping'.");

		try {
			if (waitForGlobalRequestResult() == true)
				throw new IOException(
						"Your server is alive - but buggy. "
								+ "It replied with SSH_MSG_REQUEST_SUCCESS when it actually should not.");

		} catch (IOException e) {
			throw (IOException) new IOException("The ping request failed.")
					.initCause(e);
		}
	}

	public void requestPTY(Channel c, String term, int term_width_characters,
			int term_height_characters, int term_width_pixels,
			int term_height_pixels, byte[] terminal_modes) throws IOException {
		PacketSessionPtyRequest spr;

		synchronized (c) {
			if (c.state != Channel.STATE_OPEN)
				throw new IOException("Cannot request PTY on this channel ("
						+ c.getReasonClosed() + ")");

			spr = new PacketSessionPtyRequest(c.remoteID, true, term,
					term_width_characters, term_height_characters,
					term_width_pixels, term_height_pixels, terminal_modes);

			c.successCounter = c.failedCounter = 0;
		}

		synchronized (c.channelSendLock) {
			if (c.closeMessageSent)
				throw new IOException("Cannot request PTY on this channel ("
						+ c.getReasonClosed() + ")");
			tm.sendMessage(spr.getPayload());
		}

		try {
			if (waitForChannelRequestResult(c) == false)
				throw new IOException("The server denied the request.");
		} catch (IOException e) {
			throw (IOException) new IOException("PTY request failed")
					.initCause(e);
		}
	}

	public void requestShell(Channel c) throws IOException {
		PacketSessionStartShell sm;

		synchronized (c) {
			if (c.state != Channel.STATE_OPEN)
				throw new IOException("Cannot start shell on this channel ("
						+ c.getReasonClosed() + ")");

			sm = new PacketSessionStartShell(c.remoteID, true);

			c.successCounter = c.failedCounter = 0;
		}

		synchronized (c.channelSendLock) {
			if (c.closeMessageSent)
				throw new IOException("Cannot start shell on this channel ("
						+ c.getReasonClosed() + ")");
			tm.sendMessage(sm.getPayload());
		}

		try {
			if (waitForChannelRequestResult(c) == false)
				throw new IOException("The server denied the request.");
		} catch (IOException e) {
			throw (IOException) new IOException("The shell request failed.")
					.initCause(e);
		}
	}

	public void requestSubSystem(Channel c, String subSystemName)
			throws IOException {
		PacketSessionSubsystemRequest ssr;

		synchronized (c) {
			if (c.state != Channel.STATE_OPEN)
				throw new IOException(
						"Cannot request subsystem on this channel ("
								+ c.getReasonClosed() + ")");

			ssr = new PacketSessionSubsystemRequest(c.remoteID, true,
					subSystemName);

			c.successCounter = c.failedCounter = 0;
		}

		synchronized (c.channelSendLock) {
			if (c.closeMessageSent)
				throw new IOException(
						"Cannot request subsystem on this channel ("
								+ c.getReasonClosed() + ")");
			tm.sendMessage(ssr.getPayload());
		}

		try {
			if (waitForChannelRequestResult(c) == false)
				throw new IOException("The server denied the request.");
		} catch (IOException e) {
			throw (IOException) new IOException("The subsystem request failed.")
					.initCause(e);
		}
	}

	public void requestX11(Channel c, boolean singleConnection,
			String x11AuthenticationProtocol, String x11AuthenticationCookie,
			int x11ScreenNumber) throws IOException {
		PacketSessionX11Request psr;

		synchronized (c) {
			if (c.state != Channel.STATE_OPEN)
				throw new IOException("Cannot request X11 on this channel ("
						+ c.getReasonClosed() + ")");

			psr = new PacketSessionX11Request(c.remoteID, true,
					singleConnection, x11AuthenticationProtocol,
					x11AuthenticationCookie, x11ScreenNumber);

			c.successCounter = c.failedCounter = 0;
		}

		synchronized (c.channelSendLock) {
			if (c.closeMessageSent)
				throw new IOException("Cannot request X11 on this channel ("
						+ c.getReasonClosed() + ")");
			tm.sendMessage(psr.getPayload());
		}

		if (log.isEnabled())
			log.log(50, "Requesting X11 forwarding (Channel " + c.localID + "/"
					+ c.remoteID + ")");

		try {
			if (waitForChannelRequestResult(c) == false)
				throw new IOException("The server denied the request.");
		} catch (IOException e) {
			throw (IOException) new IOException("The X11 request failed.")
					.initCause(e);
		}
	}

	public void resizePTY(Channel c, int term_width_characters,
			int term_height_characters, int term_width_pixels,
			int term_height_pixels) throws IOException {
		PacketSessionPtyResize spr;

		synchronized (c) {
			if (c.state != Channel.STATE_OPEN)
				throw new IOException("Cannot request PTY on this channel ("
						+ c.getReasonClosed() + ")");

			spr = new PacketSessionPtyResize(c.remoteID, term_width_characters,
					term_height_characters, term_width_pixels,
					term_height_pixels);
			c.successCounter = c.failedCounter = 0;
		}

		synchronized (c.channelSendLock) {
			if (c.closeMessageSent)
				throw new IOException("Cannot request PTY on this channel ("
						+ c.getReasonClosed() + ")");
			tm.sendMessage(spr.getPayload());
		}
	}

	public void sendData(Channel c, byte[] buffer, int pos, int len)
			throws IOException {
		while (len > 0) {
			int thislen = 0;
			byte[] msg;

			synchronized (c) {
				while (true) {
					if (c.state == Channel.STATE_CLOSED)
						throw new IOException("SSH channel is closed. ("
								+ c.getReasonClosed() + ")");

					if (c.state != Channel.STATE_OPEN)
						throw new IOException("SSH channel in strange state. ("
								+ c.state + ")");

					if (c.remoteWindow != 0)
						break;

					try {
						c.wait();
					} catch (InterruptedException ignore) {
					}
				}

				/* len > 0, no sign extension can happen when comparing */

				thislen = (c.remoteWindow >= len) ? len : (int) c.remoteWindow;

				int estimatedMaxDataLen = c.remoteMaxPacketSize
						- (tm.getPacketOverheadEstimate() + 9);

				/* The worst case scenario =) a true bottleneck */

				if (estimatedMaxDataLen <= 0) {
					estimatedMaxDataLen = 1;
				}

				if (thislen > estimatedMaxDataLen)
					thislen = estimatedMaxDataLen;

				c.remoteWindow -= thislen;

				msg = new byte[1 + 8 + thislen];

				msg[0] = Packets.SSH_MSG_CHANNEL_DATA;
				msg[1] = (byte) (c.remoteID >> 24);
				msg[2] = (byte) (c.remoteID >> 16);
				msg[3] = (byte) (c.remoteID >> 8);
				msg[4] = (byte) (c.remoteID);
				msg[5] = (byte) (thislen >> 24);
				msg[6] = (byte) (thislen >> 16);
				msg[7] = (byte) (thislen >> 8);
				msg[8] = (byte) (thislen);

				System.arraycopy(buffer, pos, msg, 9, thislen);
			}

			synchronized (c.channelSendLock) {
				if (c.closeMessageSent == true)
					throw new IOException("SSH channel is closed. ("
							+ c.getReasonClosed() + ")");

				tm.sendMessage(msg);
			}

			pos += thislen;
			len -= thislen;
		}
	}

	public void sendEOF(Channel c) throws IOException {
		byte[] msg = new byte[5];

		synchronized (c) {
			if (c.state != Channel.STATE_OPEN)
				return;

			msg[0] = Packets.SSH_MSG_CHANNEL_EOF;
			msg[1] = (byte) (c.remoteID >> 24);
			msg[2] = (byte) (c.remoteID >> 16);
			msg[3] = (byte) (c.remoteID >> 8);
			msg[4] = (byte) (c.remoteID);
		}

		synchronized (c.channelSendLock) {
			if (c.closeMessageSent == true)
				return;
			tm.sendMessage(msg);
		}

		if (log.isEnabled())
			log.log(50, "Sent EOF (Channel " + c.localID + "/" + c.remoteID
					+ ")");
	}

	public void sendOpenConfirmation(Channel c) throws IOException {
		PacketChannelOpenConfirmation pcoc = null;

		synchronized (c) {
			if (c.state != Channel.STATE_OPENING)
				return;

			c.state = Channel.STATE_OPEN;

			pcoc = new PacketChannelOpenConfirmation(c.remoteID, c.localID,
					c.localWindow, c.localMaxPacketSize);
		}

		synchronized (c.channelSendLock) {
			if (c.closeMessageSent == true)
				return;
			tm.sendMessage(pcoc.getPayload());
		}
	}

	public void unRegisterX11Cookie(String hexFakeCookie, boolean killChannels) {
		if (hexFakeCookie == null)
			throw new IllegalStateException("hexFakeCookie may not be null");

		synchronized (x11_magic_cookies) {
			x11_magic_cookies.remove(hexFakeCookie);
		}

		if (killChannels == false)
			return;

		if (log.isEnabled())
			log.log(50, "Closing all X11 channels for the given fake cookie");

		Vector channel_copy;

		synchronized (channels) {
			channel_copy = (Vector) channels.clone();
		}

		for (int i = 0; i < channel_copy.size(); i++) {
			Channel c = (Channel) channel_copy.elementAt(i);

			synchronized (c) {
				if (hexFakeCookie.equals(c.hexX11FakeCookie) == false)
					continue;
			}

			try {
				closeChannel(
						c,
						"Closing X11 channel since the corresponding session is closing",
						true);
			} catch (IOException e) {
			}
		}
	}

	private final boolean waitForChannelRequestResult(Channel c)
			throws IOException {
		synchronized (c) {
			while ((c.successCounter == 0) && (c.failedCounter == 0)) {
				if (c.state != Channel.STATE_OPEN) {
					String detail = c.getReasonClosed();

					if (detail == null)
						detail = "state: " + c.state;

					throw new IOException("This SSH2 channel is not open ("
							+ detail + ")");
				}

				try {
					c.wait();
				} catch (InterruptedException ignore) {
				}
			}

			if ((c.failedCounter == 0) && (c.successCounter == 1))
				return true;

			if ((c.failedCounter == 1) && (c.successCounter == 0))
				return false;

			throw new IOException("Illegal state. The server sent "
					+ c.successCounter + " SSH_MSG_CHANNEL_SUCCESS and "
					+ c.failedCounter + " SSH_MSG_CHANNEL_FAILURE messages.");
		}
	}

	/**
	 * Wait until for a condition.
	 * 
	 * @param c
	 *            Channel
	 * @param timeout
	 *            in ms, 0 means no timeout.
	 * @param condition_mask
	 *            minimum event mask
	 * @return all current events
	 * 
	 */
	public int waitForCondition(Channel c, long timeout, int condition_mask) {
		long end_time = 0;
		boolean end_time_set = false;

		synchronized (c) {
			while (true) {
				int current_cond = 0;

				int stdoutAvail = c.stdoutWritepos - c.stdoutReadpos;
				int stderrAvail = c.stderrWritepos - c.stderrReadpos;

				if (stdoutAvail > 0)
					current_cond = current_cond | ChannelCondition.STDOUT_DATA;

				if (stderrAvail > 0)
					current_cond = current_cond | ChannelCondition.STDERR_DATA;

				if (c.EOF)
					current_cond = current_cond | ChannelCondition.EOF;

				if (c.getExitStatus() != null)
					current_cond = current_cond | ChannelCondition.EXIT_STATUS;

				if (c.getExitSignal() != null)
					current_cond = current_cond | ChannelCondition.EXIT_SIGNAL;

				if (c.state == Channel.STATE_CLOSED)
					return current_cond | ChannelCondition.CLOSED
							| ChannelCondition.EOF;

				if ((current_cond & condition_mask) != 0)
					return current_cond;

				if (timeout > 0) {
					if (!end_time_set) {
						end_time = System.currentTimeMillis() + timeout;
						end_time_set = true;
					} else {
						timeout = end_time - System.currentTimeMillis();

						if (timeout <= 0)
							return current_cond | ChannelCondition.TIMEOUT;
					}
				}

				try {
					if (timeout > 0)
						c.wait(timeout);
					else
						c.wait();
				} catch (InterruptedException e) {
				}
			}
		}
	}

	private final boolean waitForGlobalRequestResult() throws IOException {
		synchronized (channels) {
			while ((globalSuccessCounter == 0) && (globalFailedCounter == 0)) {
				if (shutdown) {
					throw new IOException("The connection is being shutdown");
				}

				try {
					channels.wait();
				} catch (InterruptedException ignore) {
				}
			}

			if ((globalFailedCounter == 0) && (globalSuccessCounter == 1))
				return true;

			if ((globalFailedCounter == 1) && (globalSuccessCounter == 0))
				return false;

			throw new IOException("Illegal state. The server sent "
					+ globalSuccessCounter + " SSH_MSG_REQUEST_SUCCESS and "
					+ globalFailedCounter
					+ " SSH_MSG_REQUEST_FAILURE messages.");
		}
	}

	private void waitUntilChannelOpen(Channel c) throws IOException {
		synchronized (c) {
			while (c.state == Channel.STATE_OPENING) {
				try {
					c.wait();
				} catch (InterruptedException ignore) {
				}
			}

			if (c.state != Channel.STATE_OPEN) {
				removeChannel(c.localID);

				String detail = c.getReasonClosed();

				if (detail == null)
					detail = "state: " + c.state;

				throw new IOException("Could not open channel (" + detail + ")");
			}
		}
	}
}
